/*
 * Copyright 2008 Nathan C Jones
 * 
 * This file is part of Fivehundred.
 * 
 * Fivehundred is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * Fivehundred is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * Fivehundred. If not, see <http://www.gnu.org/licenses/>. 
 */

package com.googlecode.fivehundred.svg;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import com.googlecode.fivehundred.model.Card;
import com.googlecode.fivehundred.model.CardRenderer;

/**
 * Renders a card by dynamically constructing the card image. Generated images
 * and rasterized image components are cached.
 * 
 * @author Nathan C Jones
 */
abstract class DynamicCardRenderer implements CardRenderer {

	// TODO horizontal and vertical measurements should use the same units!

	/** The width of the small corner suit as a ratio of the card width. */
	protected static final double SCALE_SUIT_SMALL = 0.12d;

	/** The width of the card's suit symbols as a ratio of the width of the card. */
	protected static final double SCALE_SUIT_MEDIUM = 0.24d;

	/**
	 * The width of the largest suit (eg. ace of spades) as a ratio of the card
	 * width.
	 */
	protected static final double SCALE_SUIT_LARGE = 0.9d;

	/** The width of the number as a ratio of the width of the card. */
	protected static final double SCALE_NUMBER = 0.12d;

	/**
	 * The size of the margin at the side of the card as a ratio of the width of
	 * the card.
	 */
	protected static final double MARGIN_WIDTH = 0.155d;

	/**
	 * The size of the margin at the top and bottom ends of the card as a ratio
	 * of the height of the card.
	 */
	protected static final double MARGIN_HEIGHT = 0.11d;

	/**
	 * The size of the margin between the top of the card and the center of the
	 * number.
	 */
	protected static final double MARGIN_NUMBER_HEIGHT = 0.10d;

	/**
	 * The size of the margin between the top of the card and the center of the
	 * small corner suit.
	 */
	protected static final double MARGIN_SUIT_HEIGHT = 0.23d;

	/**
	 * The size of the margin between the side of the card and the center of the
	 * number and small suit.
	 */
	protected static final double MARGIN_INNER_WIDTH = 0.08d;

	/** The image cache. */
	private static final Map<String, Image> imageCache = new HashMap<String, Image>();

	/** Rasteriser for generating images from SVG files. */
	private static final SVGRasterizer rasteriser = new SVGRasterizer();

	private final Card card;

	/**
	 * Create a card renderer from the given card.
	 * 
	 * @param card the card to create a renderer for.
	 */
	public DynamicCardRenderer(Card card) {
		this.card = card;
	}

	protected Image getSvgImage(int width, String svgFile) {
		URL imgURL = ClassLoader.getSystemResource(svgFile);
		if (imgURL == null) {
			throw new RuntimeException("Missing svg resource: " + svgFile);
		}
		String imageKey = svgFile + width;
		Image image = imageCache.get(imageKey);
		if (image == null) {
			image = rasteriser.createImage(imgURL, width);
			imageCache.put(imageKey, image);
		}
		return image;
	}

	protected Image getSuitImage(Card card, int width) {
		// TODO rename svg files to match the case of the Suit.toString() value.
		String svgFile = card.getSuit().toString().toLowerCase().substring(0, card.getSuit().toString().length() - 1) + ".svg";
		return getSvgImage(width, svgFile);
	}

	public int getHeight(int width) {
		return (int) (DEFAULT_WIDTH * Math.sqrt(2d));
	}

	/**
	 * {@inheritdoc}
	 */
	public Image getImage() {
		return getImage(DEFAULT_WIDTH, DEFAULT_HEIGHT);
	}

	/**
	 * {@inheritdoc}
	 */
	public Image getImage(int width, int height) {
		if (width < 0) {
			throw new IllegalArgumentException("Width of a card image must be non-negative.");
		}
		if (height < 0) {
			throw new IllegalArgumentException("Height of card image must be non-negative.");
		}
		String imageId = card.toString() + "(" + width + "," + height + ")";
		Image image = imageCache.get(imageId);
		if (image == null) {
			image = generateImage(width, height);
			imageCache.put(imageId, image);
		}
		return image;
	}

	/**
	 * @return the card that is being rendered.
	 */
	public Card getCard() {
		return card;
	}

	/**
	 * Generate the image for the card.
	 * 
	 * @param width the width of the image to generate.
	 * @param height the height of the image to generate.
	 * @return an image of the card with the given width and height.
	 */
	protected Image generateImage(final int width, final int height) {
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g2 = image.createGraphics();

		// draw corner suit and number
		Image numberImage = getCardNumberImage(width);
		Image invertedNumberImage = createInvertedImage(numberImage);
		int numberX = (int) (width * MARGIN_INNER_WIDTH - numberImage.getWidth(null) / 2d);
		int numberY = (int) (height * MARGIN_NUMBER_HEIGHT - numberImage.getHeight(null) / 2d);
		g2.drawImage(numberImage, numberX, numberY, null);

		int numberX2 = (int) (width * (1 - MARGIN_INNER_WIDTH) - numberImage.getWidth(null) / 2d);
		int numberY2 = (int) (height * (1 - MARGIN_NUMBER_HEIGHT) - numberImage.getHeight(null) / 2d);
		g2.drawImage(invertedNumberImage, numberX2, numberY2, null);

		Image suitImageSmall = getSuitImage(card, (int) (SCALE_SUIT_SMALL * width));
		Image suitImageSmallInverted = createInvertedImage(suitImageSmall);
		int suitSmallX = (int) (width * MARGIN_INNER_WIDTH - suitImageSmall.getWidth(null) / 2d);
		int suitSmallY = (int) (height * MARGIN_SUIT_HEIGHT - suitImageSmall.getWidth(null) / 2d);
		g2.drawImage(suitImageSmall, suitSmallX, suitSmallY, null);
		int suitSmallX2 = (int) (width * (1 - MARGIN_INNER_WIDTH) - suitImageSmallInverted.getWidth(null) / 2d);
		int suitSmallY2 = (int) (height * (1 - MARGIN_SUIT_HEIGHT) - suitImageSmall.getHeight(null) / 2d);
		g2.drawImage(suitImageSmallInverted, suitSmallX2, suitSmallY2, null);

		drawCardCenter(g2, width, height);
		return image;
	}

	/**
	 * Invert an image.
	 * 
	 * @param image the image to invert.
	 * @return a new image which is a 180 degree rotation of the original around
	 *         its centre.
	 */
	private BufferedImage createInvertedImage(final Image image) {
		BufferedImage invertedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
		((Graphics2D) invertedImage.getGraphics()).drawImage(image, AffineTransform.getRotateInstance(Math.PI, 0.5 * image.getWidth(null),
				0.5 * image.getHeight(null)), null);
		return invertedImage;
	}

	private Image getCardNumberImage(final int width) {
		return getSvgImage((int) (width * SCALE_NUMBER), getCardNumberImageFileName());
	}

	private String getCardNumberImageFileName() {
		return card.getFace().toString().toLowerCase() + "_" + card.getSuit().getColour().toString().toLowerCase() + ".svg";
	}

	/**
	 * Draw the center of the card onto the graphics.
	 * 
	 * @param g the graphics to draw the card center onto.
	 * @param width the width of the card graphics.
	 * @param height the width of the card graphics.
	 */
	protected abstract void drawCardCenter(Graphics2D g, int width, int height);

	/**
	 * A coordinate indicating a point relative to the bounds of the card area.
	 * 
	 * @author Nathan C Jones
	 */
	static class RelativeCoordinate extends Point2D.Double {

		private static final long serialVersionUID = 1L;

		// TODO replace references to RelativeCoordinate with Point2D.Double
		RelativeCoordinate(double x, double y) {
			super(x, y);
		}
	}
}
